/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.shephertz.appwarp.websocket.binary;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.CumulativeProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

/**
 * Decodes incoming buffers in a manner that makes the sender transparent to the
 * decoders further up in the filter chain. If the sender is a native client then
 * the buffer is simply passed through. If the sender is a websocket, it will extract
 * the content out from the dataframe and parse it before passing it along the filter
 * chain.
 *
 * @author DHRUV CHOPRA
 */
public class WebSocketDecoder extends CumulativeProtocolDecoder {
	static Logger LOGCOMM = LoggerFactory.getLogger("comm");
	CharsetDecoder charsetDecoder = Charset.forName("UTF-8").newDecoder();

    @Override
    protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {
        Object result;

        // 检查 session 属性中是否有 "isWEB" ?
        if(!session.containsAttribute(WebSocketUtils.SessionAttribute)) {
            // first message on a new connection. see if its from a websocket or a native socket.
        	// 没有 "isWEB"，说明 session 刚刚建立，这是第一条消息，要检查它是来自 WebSocket 还是一个原生的 socket

        	// 通过尝试 WebSocket 握手，来判断是否来自 WebSocket
            if(tryWebSockeHandShake(session, in, out)) {
                // websocket handshake was successful. Don't write anything to output
                // as we want to abstract the handshake request message from the handler.
                in.position(in.limit());
                return true;
            }
            else {
                // message is from a native socket. Simply wrap and pass through.
            	result = IoBuffer.wrap(in.array(), 0, in.limit());
                in.position(in.limit());

                // 是 native socket 连接，设置 "isWEB" == false
                session.setAttribute(WebSocketUtils.SessionAttribute, false);
            }
        }
        else if(true == (Boolean)session.getAttribute(WebSocketUtils.SessionAttribute)) {
        	// "isWEB" == false
            // there is incoming data from the websocket. Decode and send to handler or next filter.
            int startPos = in.position();
            result = buildWSDataBuffer(in, session);
            if(result == null){
                // There was not enough data in the buffer to parse. Reset the in buffer
                // position and wait for more data before trying again.
                in.position(startPos);
                return false;
            }
        }
        else{
            // session is known to be from a native socket. So
            // simply wrap and pass through.
        	result = IoBuffer.wrap(in.array(), 0, in.limit());
            in.position(in.limit());
        }

        out.write(result);
        return true;
    }

    /**
     * Try parsing the message as a websocket handshake request. If it is such
     * a request, then send the corresponding handshake response (as in Section 4.2.2 RFC 6455).
     * 尝试把消息当作 WebSocket 握手消息来解析。如果解析成功，则发送对应的握手响应（见 RFC6455 中 4.2.2 章节）。
     */
    private boolean tryWebSockeHandShake(IoSession session, IoBuffer in, ProtocolDecoderOutput out) {
        try{
            String payLoadMsg = new String(in.array());
            String socketKey = WebSocketUtils.getClientWSRequestKey(payLoadMsg);
            if(socketKey.length() <= 0){
                return false;
            }
            String challengeAccept = WebSocketUtils.getWebSocketKeyChallengeResponse(socketKey);
            WebSocketHandShakeResponse wsResponse = WebSocketUtils.buildWSHandshakeResponse(challengeAccept);
            session.setAttribute(WebSocketUtils.SessionAttribute, true);
            session.write(wsResponse);
            return true;
        }
        catch(Exception e){
            // input is not a websocket handshake request.
            return false;
        }
    }

    /**
     * Decode the in buffer according to the Section 5.2. RFC 6455
     * If there are multiple websocket dataframes in the buffer, this will parse
     * all and return one complete decoded buffer.
     */
    private Object buildWSDataBuffer(IoBuffer in, IoSession session) throws Exception {
    	IoBuffer buff = null;
        boolean isText = true;

        do {
        	// 第 0 字节
            byte b0 = in.get();
            boolean fin = (b0 & 0x80) != 0;   // 是否是当前消息的最后一个 fragment
            int opCode = (byte) (b0 & 0x0F);  // OPCODE

            if(!fin) {
            	// FIN == 0 时怎么处理？
            	LOGCOMM.warn("FIN = 0 !");
            }

            if(opCode == WebSocketUtils.OC_CONNECTION_CLOSE) {
            	// OPCODE 指示关闭连接
            	//LOGCOMM.debug("OC_CONNECTION_CLOSE");
                session.close(true);
                // 即使连接被关闭，已经解析的数据还是要返回？
                // 如果是文本数据，则转换为字符串再返回
            	return (isText && (buff != null)) ? buff.getString(charsetDecoder) : buff;
            } else if(opCode == WebSocketUtils.OC_TEXT_FRAME) {
            	// 文本数据帧
            	//LOGCOMM.debug("OC_TEXT_FRAME");
            	isText = true;
            } else if(opCode == WebSocketUtils.OC_BINARY_FRAME) {
            	// 二进制数据帧
            	//LOGCOMM.debug("OC_BINARY_FRAME");
            	isText = false;
            } else if(opCode == WebSocketUtils.OC_CONTINUATION_FRAME) {
            	// 后续帧 怎么处理？
            	LOGCOMM.warn("OC_CONTINUATION_FRAME");
            } else if(opCode == WebSocketUtils.OC_PIN) {
            	// PING 怎么处理？
            	LOGCOMM.warn("OC_PIN");
            } else if(opCode == WebSocketUtils.OC_PON) {
            	// PONG 怎么处理？
            	LOGCOMM.warn("OC_PON");
            }

            // 第 1 字节
            byte b1 = in.get();
            // MASK 标志
            boolean mask = (b1 & 0x80) != 0;
            // 净荷长度指示
            int payloadLen = b1 & 0x7F;

            // 看是否有扩展净荷长度
            if(payloadLen == WebSocketUtils.PAYLOAD_LENGTH_23_BITS) {
            	// 后面的 2 字节是真正的长度(网络字节序)
            	payloadLen = in.getUnsignedShort();
            }
            else if(payloadLen == WebSocketUtils.PAYLOAD_LENGTH_71_BITS) {
            	// 后面的 8 字节是真正的长度(网络字节序)
            	payloadLen = (int)(in.getLong() & Integer.MAX_VALUE);
            }

            if(payloadLen > 64*1024) {
            	LOGCOMM.warn("超长帧: {} 字节！", payloadLen);
            }

            // Mask-Key
            byte maskKey[] = null;
            if(mask) {
                // 取出 maskKey(固定4字节)
            	maskKey = new byte[4];
            	in.get(maskKey);
            }

            // Validate if we have enough data in the buffer to completely
            // parse the WebSocket DataFrame. If not return null.
            // 检查当前缓冲区中是否有足够的数据用来解析当前 WebSocket 数据帧
            if(payloadLen > in.remaining()) {
            	LOGCOMM.debug("数据不足: {}/{}，继续等待 ...", in.remaining(), payloadLen);
                return null;
            }

            // 净荷数据
            byte[] payload = new byte[payloadLen];

            if(mask) {
	            // 逐个字节进行 UNMASK 运算
	            for (int i = 0; i < payloadLen; i++) {
	                byte maskedByte = in.get();
	                payload[i] = (byte) (maskedByte ^ maskKey[i % 4]);
	            }
            } else {
            	// 净荷未被 MASK 过，可以直接读取
            	in.get(payload);
            }

            if(buff == null) {
                buff = IoBuffer.wrap(payload);
                buff.position(buff.limit());
                buff.setAutoExpand(true);
            } else {
                buff.put(payload);
            }
        } while(in.hasRemaining()); // 如果还有下一条消息的数据，则继续解码？

        // 翻转缓冲区
        buff.flip();
        // 如果是文本数据，则转换为字符串再返回
    	return isText ? buff.getString(charsetDecoder) : buff;
    }
}
